using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;

namespace csammisrun.shaim
{
  /// <summary>
  /// Describes the types of network proxies that shaim plugins should support
  /// </summary>
  public enum ShaimProxyType
  {
    /// <summary>
    /// Direct connection to the network
    /// </summary>
    None,
    /// <summary>
    /// Route socket connections through a SOCKS5 proxy
    /// </summary>
    SOCKS5
  }

  /// <summary>
  /// Describes the valid SOCKS5 authentication methods
  /// </summary>
  enum SOCKS5AuthType : byte
  {
    None = 0x00,
    GSSAPI = 0x01,
    UsernamePassword = 0x02,
    AuthMethodRejected = 0xFF
  }

  partial class Core
  {
    public ShaimProxyInfo GlobalProxySetting
    {
      get { return _proxyinfo; }
    }

    /// <summary>
    /// Initialize the proxy settings with information from the preference file
    /// </summary>
    /// <remarks>If the required proxy settings are missing or malformed, the global proxy is set to "None"</remarks>
    private void LoadProxyInfo()
    {
      _proxyinfo = new ShaimProxyInfo();
      Dictionary<string, string> settings = LoadAllSettings("GlobalProxy");
      if (settings.ContainsKey("ProxyType"))
      {
        try
        {
          _proxyinfo.ProxyType = (ShaimProxyType)Enum.Parse(typeof(ShaimProxyType), settings["ProxyType"]);
        }
        catch (Exception)
        {
          _proxyinfo.ProxyType = ShaimProxyType.None;
          return;
        }
      }

      if (settings.ContainsKey("Port"))
      {
        try
        {
          _proxyinfo.Port = Int32.Parse(settings["Port"]);
          if (_proxyinfo.Port <= 0 || _proxyinfo.Port >= 65536)
          {
            throw new ArgumentOutOfRangeException();
          }
        }
        catch (Exception)
        {
          _proxyinfo.ProxyType = ShaimProxyType.None;
          return;
        }
      }

      if (settings.ContainsKey("Server"))
      {
        _proxyinfo.Server = settings["Server"];
      }
      else
      {
        _proxyinfo.ProxyType = ShaimProxyType.None;
        return;
      }

      if (settings.ContainsKey("Username"))
      {
        _proxyinfo.Username = settings["Username"];
      }
      if (settings.ContainsKey("Password"))
      {
        _proxyinfo.Password = settings["Password"];
      }
    }

    /// <summary>
    /// Writes the current global proxy settings to the preference file
    /// </summary>
    private void SaveProxyInfo()
    {
      SetProxyInfo(_proxyinfo.ProxyType, _proxyinfo.Server, _proxyinfo.Port, _proxyinfo.Username, _proxyinfo.Password);
    }

    public void SetProxyInfo(ShaimProxyType proxytype, string server, int port, string username, string password)
    {
      SaveSetting("GlobalProxy", "ProxyType", proxytype.ToString());
      SaveSetting("GlobalProxy", "Server", server);
      SaveSetting("GlobalProxy", "Port", port);
      SaveSetting("GlobalProxy", "Username", username);
      SaveSetting("GlobalProxy", "Password", password);
      LoadProxyInfo();
    }

    public void ConnectThroughGlobalProxy(string host, int port, Delegate callback)
    {
      // Make sure the callback is the right type
      ParameterInfo[] param = callback.Method.GetParameters();
      if (param.Length != 2 || param[0].ParameterType != typeof(Socket) || param[1].ParameterType != typeof(string))
      {
        throw new ArgumentException("Callback delegate must take a Socket and a string as its parameters");
      }

      SocketConnectionData scd = new SocketConnectionData(host, port, callback);
      scd.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

      string proxiedhost = "";
      if (GlobalProxySetting.ProxyType == ShaimProxyType.None)
      {
        proxiedhost = scd.Server;
      }
      else
      {
        proxiedhost = GlobalProxySetting.Server;
      }

      try
      {
        Dns.BeginGetHostEntry(proxiedhost, new AsyncCallback(CreateGPSEndDnsLookup), scd);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot connect to DNS service:" + Environment.NewLine +
          sockex.Message;
        callback.DynamicInvoke(scd.Socket, message);
      }
    }

    /// <summary>
    /// Ends the DNS lookup phase of connection and begins connecting the socket to the proxy host
    /// </summary>
    protected void CreateGPSEndDnsLookup(IAsyncResult res)
    {
      SocketConnectionData scd = null;

      IPHostEntry hosts = null;
      try
      {
        scd = res.AsyncState as SocketConnectionData;
        hosts = Dns.EndGetHostEntry(res);
      }
      catch (Exception)
      {
        // Fallback to a different synchronous DNS lookup method
        hosts = new IPHostEntry();
        try
        {
          hosts.AddressList = Dns.GetHostAddresses(scd.Server);
        }
        catch (Exception ex)
        {
          // Well shit
          string message = "Cannot resolve server:" + Environment.NewLine + ex.Message;
          scd.Callback.DynamicInvoke(scd.Socket, message);
        }
      }

      int port = 0;
      if (GlobalProxySetting.ProxyType != ShaimProxyType.None)
      {
        port = GlobalProxySetting.Port;
      }
      else
      {
        port = scd.Port;
      }

      IPAddress address = hosts.AddressList[0];
      IPEndPoint ipep = new IPEndPoint(address, port);

      try
      {
        scd.Socket.BeginConnect(ipep, new AsyncCallback(CreateGPSEndInitialConnection), scd);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot connect to server:" + Environment.NewLine +
          sockex.Message;
        scd.Callback.DynamicInvoke(scd.Socket, message);
      }
    }

    /// <summary>
    /// Ends the host connection phase and negotiates the proxied connection
    /// </summary>
    protected void CreateGPSEndInitialConnection(IAsyncResult res)
    {
      SocketConnectionData scd = null;
      try
      {
        scd = res.AsyncState as SocketConnectionData;
        scd.Socket.EndConnect(res);
      }
      catch (Exception sockex)
      {
        string message = "Can't connect to server: " + Environment.NewLine +
          sockex.Message;
        scd.Callback.DynamicInvoke(scd.Socket, message);
        return;
      }

      if (GlobalProxySetting.ProxyType == ShaimProxyType.SOCKS5)
      {
        SOCKS5AuthType auth = GetSOCKS5AuthMethod(scd);
        if (auth == SOCKS5AuthType.AuthMethodRejected)
        {
          scd.Socket.Close();
          scd.Callback.DynamicInvoke(scd.Socket, "SOCKS5 authentication mechanism unsupported");
          return;
        }
        else if (auth != SOCKS5AuthType.None)
        {
          if (SendSOCKS5Authentication(scd, auth) == false)
          {
            scd.Socket.Close();
            scd.Callback.DynamicInvoke(scd.Socket, "SOCK5 authentication failed, bad username/password");
            return;
          }
        }

        if (SendSOCKS5ConnectRequest(scd) == false)
        {
          scd.Socket.Close();
          scd.Callback.DynamicInvoke(scd.Socket, "SOCKS5 connect failed, proxy server failure");
          return;
        }
      }

      // Done!
      scd.Callback.DynamicInvoke(scd.Socket, "");
    }

    /// <summary>
    /// Queries the SOCKS5 proxy for its desired authentication mechanism
    /// </summary>
    private SOCKS5AuthType GetSOCKS5AuthMethod(SocketConnectionData scd)
    {
      int reqlength = 4, sent = 0;
      byte[] request = new byte[reqlength];
      request[0] = 0x05;
      request[1] = 0x02;
      request[2] = 0x00;
      request[3] = 0x02;
      while (sent < reqlength)
      {
        sent += scd.Socket.Send(request, sent, reqlength - sent, SocketFlags.None);
      }

      int replength = 2, received = 0;
      byte[] reply = new byte[replength];
      while (received < replength)
      {
        received += scd.Socket.Receive(reply, received, replength - received, SocketFlags.None);
      }

      return (SOCKS5AuthType)reply[1];
    }

    /// <summary>
    /// Sends the authentication sequence to the proxy server
    /// </summary>
    private bool SendSOCKS5Authentication(SocketConnectionData scd, SOCKS5AuthType authtype)
    {
      int usernamelen = Encoding.ASCII.GetByteCount(GlobalProxySetting.Username);
      int passwordlen = Encoding.ASCII.GetByteCount(GlobalProxySetting.Password);
      int reqlength = 3 + usernamelen + passwordlen, sent = 0;
      byte[] request = new byte[reqlength];
      request[0] = 0x05;
      request[1] = (byte)usernamelen;
      Array.ConstrainedCopy(Encoding.ASCII.GetBytes(GlobalProxySetting.Username), 0, request, 2, usernamelen);
      request[2 + usernamelen] = (byte)passwordlen;
      Array.ConstrainedCopy(Encoding.ASCII.GetBytes(GlobalProxySetting.Password), 0, request, 3 + usernamelen, passwordlen);

      while (sent < reqlength)
      {
        sent += scd.Socket.Send(request, sent, reqlength - sent, SocketFlags.None);
      }

      int replength = 2, received = 0;
      byte[] reply = new byte[replength];
      while (received < replength)
      {
        received += scd.Socket.Receive(reply, received, replength - received, SocketFlags.None);
      }

      return (reply[1] == 0x00);
    }

    /// <summary>
    /// Send a CONNECT request to the proxy server
    /// </summary>
    private bool SendSOCKS5ConnectRequest(SocketConnectionData scd)
    {
      IPAddress ipaddr = null;
      try
      {
        ipaddr = Dns.GetHostEntry(scd.Server).AddressList[0];
      }
      catch (FormatException)
      {
        // We'll just use the domain name method then
      }

      int sent = 0;
      int iplength = (ipaddr == null) ? Encoding.ASCII.GetByteCount(scd.Server) :
        (ipaddr.AddressFamily == AddressFamily.InterNetwork) ? 4 : 6;

      int reqlength = 6 + iplength;
      byte[] request = new byte[reqlength];
      request[0] = 0x05;
      request[1] = 0x01;
      request[2] = 0x00;
      if (ipaddr == null)
      {
        request[3] = 0x03;
        Array.ConstrainedCopy(Encoding.ASCII.GetBytes(scd.Server), 0, request, 4, reqlength - 6);

      }
      else if (ipaddr.AddressFamily == AddressFamily.InterNetwork)
      {
        request[3] = 0x01;
        Array.ConstrainedCopy(ipaddr.GetAddressBytes(), 0, request, 4, reqlength - 6);
      }
      else
      {
        request[3] = 0x04;
        Array.ConstrainedCopy(ipaddr.GetAddressBytes(), 0, request, 4, reqlength - 6);
      }

      request[reqlength - 2] = (byte)((scd.Port & 0xFF00) >> 8);
      request[reqlength - 1] = (byte)(scd.Port & 0xFF);
      while (sent < reqlength)
      {
        sent += scd.Socket.Send(request, sent, reqlength - sent, SocketFlags.None);
      }

      byte[] reply = new byte[256]; // Variable length reply here
      scd.Socket.Receive(reply);
      return (reply[1] == 0x00);
    }
  }
}
